半小时了解TCP

TCP Transmission Control Protocol作为传输层的核心协议,为支撑起互联网络作出了重大贡献,如今我们只要使用互联网几乎就离不开TCP。了解TCP的基本原理对我们的软件架构设计、开发、运维测试都有裨益。

TCP v.s. UDP

先看看TCP与UDP的区别,同为传输层的协议,每次提到TCP都会把UDP拿出来欺负一番。

- TCP UDP
报文发送 面向连接,根据连接的情况动态调整 无连接,程序让发多少发多少
资源要求 较多,需建立连接 较少
头部大小 20字节 8字节
传输模式 字节流模式,使用缓存 数据报模式,即成即走
有序可靠 多种机制保证

三次握手

三次握手用于建立TCP连接,从而分配对应的连接资源。

图解如下:
img

  1. Client发送SYN包(seq=i)。

    1
    2
    3
    4
    // 状态标志
    C_STATUS=SYN_SENT
    // 报文内容
    SYN=1, seq=i
  2. Server接收并发送一个新的SYN包(seq=j),同时该包确认上一个SYN包(ack=i+1)。

    1
    2
    3
    4
    // 状态标志
    S_STATUS=SYN_RECV
    // 报文内容
    SYN=1, ACK=1, seq=j, ack=i+1
  3. Client接收并发送确认新SYN包的ACK包(seq=i+1, ack=j+1),Server接受ACK包。

    1
    2
    3
    4
    // 状态标志
    C_STATUS=ESTABLISHED,S_STATUS=ESTABLISHED
    // 报文内容
    ACK=1, seq=i+1, ack=j+1

What

三次握手其实做了8件事:

  1. 第一次握手:
    Client第一次“伸手”,将请求发出。
  2. 第二次握手:
    Server收到第一次“伸手”后“回握”Client,Server验证了Client的发送能力、以及Server自身的接收能力。
  3. 第三次握手:
    Client收到“回握”后,验证了自身的发送和接收能力、以及Server的接收和发送能力,因此Client认为是时候建立连接了,与Server端再次握手。
    Server收到最后这次握手后,验证了自身的发送能力、以及Client的发送能力,因此Server也认为连接建立的时机成熟了。

Why

  1. 防止Server端浪费连接资源。
    • 若只有两次握手,Client发送连接请求,Server应答并分配资源,若ACK没有到达Client端,Client端认为连接未建立,而Server端认为已建立,Server端将保留分配的资源。
  2. 可能死锁。
    • 若只有两次握手,Client发送连接请求,Server应答,若ACK没有到达Client端,此时Server认为连接已建立,Client认为未建立,Server开始尝试发送数据:Client继续等待请求应答,忽略Server发送的数据;Server在等待ACK超时后继续重复发送。

四次挥手

四次挥手用于断开TCP连接,从而释放对应的连接资源。

图解如下:
img

  1. Client发送标记FIN的包(seq=i)。

    1
    2
    3
    4
    // 状态标志
    C_STATUS=FIN_WAIT_1
    // 报文内容
    FIN=1, seq=i
  2. Server接收并发送确认此包的ACK包(ack=i+1); Client接收此ACK包。

    1
    2
    3
    4
    // 状态标志
    S_STATUS=CLOSE_WAIT,C_STATUS=FIN_WAIT_2
    // 报文内容
    ACK=1, seq=j, ack=i+1
  3. Server发送标记FIN的包(seq=j)。

    1
    2
    3
    4
    // 状态标志
    S_STATUS=LAST_ACK
    // 报文内容
    FIN=1, seq=j+1
  4. Client接收并发送确认此包的ACK包(ack=j+1);Server接收此ACK包。

    1
    2
    3
    4
    5
    6
    // 状态标志
    C_STATUS=TIME_WAIT -> (30s) CLOSED
    * TIME_WAIT: 防止最后发送给服务端的ACK丢失,若丢失服务端将重新发送FIN包。
    S_STATUS=CLOSED
    // 报文内容
    ACK=1, seq=i+1, ack=j+2

What

四次挥手其实做了4件事:

  1. Client第一次“挥手”,此时Client已经没有待发送数据给Server了。
  2. Server接收到Client FIN包并返回ACK后,Server确认了Client发出的“Client没有给Server的待发送数据了”的消息已经被Server接收了;Client接收针对Client FIN包的ACK包后,Client确认了Client发出的“Client没有给Server的待发送数据了”的消息已经被Server接收了。
  3. Server没有待发送数据后,发送Server FIN包并被Client接收后,Client确认了“Server没有给Client的待发送数据了”的消息已经被Client接收了。
  4. Client返回最后一个ACK后,Server确认了“Server没有给Client的待发送数据了”的消息已经被Client接收了。至此,双方都知道对方没有待发送数据了,且自身没有待发送数据的消息已经送达给了对方,因此是时候关闭连接了。

Why

  • 确保全双工模式下最后的数据能完成传输。
    Server收到FIN包时,仅仅表示Client没有数据发送给Server,但未必Server所有数据都传输完毕。Server所有包发送完毕后,Server发送FIN包才能确保两端所有数据都传输完毕。

滑动窗口

滑动窗口(TCP Windowing)是一种TCP流控的方式,主要解决的问题是发送端发送消息太快,或接收端由于带宽过窄/处理能力有限,导致接收端缓存溢出,从而丢失数据。

窗口大小

发送端接收一个ACK前,最多可以发送的数据量。

过程

  • 接收端通知发送方自己的窗口大小,在ACK时交换窗口信息,窗口大小可以动态改变。
  • 窗口内的包不必等待前面的包ACK就可以发送。
  • 窗口外的包必须等待前面窗口内包的ACK才能继续发送。

发送端缓冲

在发送端的缓冲区有以下四类数据:

  1. Sent and acked(之前已传输完毕)
  2. Sent but not acked(窗口内已发送)
  3. Not sent but recipient ready to receive(窗口内剩余待发送)
  4. Not sent and recipient not ready to receive(窗口外需等待ACK才能发送)

接受端缓冲

  1. Received and acked but not processed(已接收到缓存并ACK,但暂未被上层协议处理的)
  2. Received but not acked(已接受到缓存但暂未ACK)
  3. Not received(缓存区剩余空间)

过程示例

A->B: Data, seq = 100
B->A: ACK, seq = 200, ack = 100+1, win = 3
A->B: ACK, seq = 101, ack = 200+1
A->B: Data, seq = 102
A->B: Data, seq = 103
A->B: Data, seq = 104
A hold
B->A: ACK, seq = 201, ack = 102+1, win = 1
A still hold
B->A: ACK, seq = 202, ack = 103+1, win = 2
A->B: Data, seq = 105
A->B: Data, seq = 106
A hold
// 窗口变化
100 101 | 102 103 104 | 105 106 …
100 101 102 103 104 | 105 106 | …

拥塞控制

在网络中突发大量的报文,导致网络中的路由器等传输节点的缓存耗尽,从而造成网络拥塞,因此TCP通过一系列措施对拥塞进行了控制。

拥塞窗口

发送端保持一个动态的拥塞窗口(Congestion Window),并尝试将自己的发送窗口(Send Window)与拥塞窗口保持一致,拥塞窗口变化时,发送端将相应地调整发送窗口。

控制算法

  • 发送方保存cwnd(Congestion Window)变量及慢启动门限ssthresh(slow-start thresh)变量,初始时使用慢启动算法。
  • 当cwnd < ssthresh,使用慢启动算法。
  • 当cwnd > ssthresh,使用拥塞避免算法。
  • 当cwnd = ssthresh,使用任意一种控制算法。
  • 当网络出现拥塞时,将ssthresh设置为当前发送窗口(不一定是拥塞窗口的大小)的一半,cwnd设为1,并重新使用慢启动。
慢启动

慢启动算法的思路在于一开始发送端以较小的拥塞窗口试探网络情况,如果情况尚好,即每一次发送报文都能准确收到ACK,则将拥塞窗口加倍。

示例

A init cwnd = 1, ssthresh = 4
A->B: Data 1, seq = 100
B->A: ACK
1, seq = 200, ack = 100+1
A: looking good, let’s double the cwnd! cwnd = 2
A->B: Data 2, seq = [101, 102]
B->A: ACK
2, seq = [201, 202], ack = [101+1, 102+1]
A: still looking good, double the cwnd again! cwnd = 4
A->B: Data 4, seq = [103, 104, 105, 106]
B->A: ACK
4, seq = [203, 204, 205, 206], ack = [103+1, 104+1, 105+1, 106+1]
A: still looking good, double cwnd, cwnd = 8
now cwnd > ssthresh, switching to congestion avoidance!

拥塞避免

拥塞避免算法的思路在于让cwnd增长变得缓慢,每次往返只加1,而不像慢启动时成倍地增长。

示例

A current cwnd = 8, ssthresh = 4
A->B: Data 8, seq = [107~114]
B->A: ACK
8, seq = [207~214], ack = [107+1~114+1]
A: looking good, let’s increase cwnd, cwnd = 9
… later A cwnd = 10
A->B: Data 10, seq = [124~133]
B->A: ACK < 10
A: something happened!
**
shrink ssthresh to 0.5 send window( = cwnd = 10) at 5, cwnd = 1, switching to slow start!**

错误恢复

由于在实际的网络环境中会出现丢包的情况,TCP提供了多种机制来修正错误。

重传

发送端为每个TCP连接维护一个RTO重传定时器,发送端如果发现RTO超时还没有收到ACK,将重传该报文,这是TCP提供的基本的错误恢复能力。
发送端可以多次重传未收到ACK的报文,每次重传时RTO的时间翻倍,直到达到设定的上限,或收到期望的ACK。

快速重传

显然仅用重传作为单一的错误恢复手段显得很弱,且RTO成倍增长,重传的间隔较长,因此TCP还设计了快速重传机制。
快速重传的思路在于接收端配合发送端,让发送端及早知道报文的丢失,从而进行重传,而不必每次都等待RTO。
当接收端收到非连续的乱序报文时,如果没有快速重传,应继续等待缺失的报文,而暂时不对后续的报文发送ACK;而当快速重传时,接收端对后续的报文发送ACK,ack=其期待的正常顺序的报文seq,发送端重复接收三次此类ACK后立即重传对应seq的报文。

示例

A->B: Data seq 100
B->A: ACK ack = 100+1
A->B: Data seq 101, but lost!
A->B: Data seq 102
B->A: ACK ack = 100+1, indicating Data 101 loss
A->B: Data seq 103
B->A: ACK ack = 100+1, indicating Data 101 loss
A->B: Data seq 104
B->A: ACK ack = 100+1, indicating Data 101 loss
A: 3 times duplicate ACK on Data 100, so Data 101 must be lost, retransmitting it!
A->B: retry Data seq 101

快速恢复

由于快速重传只是尝试重传,并没有对cwnd及ssthresh调整以适应网络环境,因此需要快速恢复对快速重传加强。
当启用快速恢复时,每当快速重传发生,即收到三个重复的ACK时:

  1. 立即重新发送重复ACK对应的已丢失报文。
  2. ssthresh = cwnd/2,cwnd = ssthresh 或 cwnd = ssthresh + 3(某些实现不同)。
  3. 启动快速恢复算法:
  • 发送端每收到一个重复的ACK,cwnd加1。
  • 当收到新的正常ACK时,cwnd = ssthresh,进入拥塞避免。
TCP New Reno改进

在New Reno版本中,TCP协议针对快速恢复有了改进。
在快速重传阶段,如果发送端重传了重复ACK的那一个包后,按道理来说,接收端应该会将所有暂未确认的ACK都发过来,即ack=最后接收到seq+1,这样就保持了连续性。
但是,如果丢失的报文不止一个,此时将发送端重传后收到的ACK称为Partial ACK,一旦发送端收到Partial ACK,它就可以推断丢包数大于1,从而继续重发滑动窗口内后续第一个未被ACK的包,直到没有收到预期的ACK,快上加快。

A->B: Data seq 100
B->A: ACK ack = 100+1
A->B: Data seq 101, but lost!
A->B: Data seq 102, but also lost!
B->A: ACK ack = 100+1, indicating Data 101 loss
A->B: Data seq 103
B->A: ACK ack = 100+1, indicating Data 101 loss
A->B: Data seq 104
B->A: ACK ack = 100+1, indicating Data 101 loss
A: 3 times duplicate ACK on Data 100, so Data 101 must be lost, retransmitting it!
A->B: retry Data seq 101
B->A: ACK ack = 101+1
A: hold on, I was expecting ACK on 104, so this ACK must be partial!
retransmitting Data 102!

TCP with SACK改进

虽然TCP New Reno版本中对于Partial ACK后面的包有了更高效的重传处理,但发送端对于Partial ACK后面的包的接收情况仍然不知情,只能在窗口范围内一个个地尝试重传,当有大量丢包情况时,这样的策略显得低效。
因此在TCP with SACK(Selective ACK,选择确认)版本中,接收端返回给发送端的重复ACK包中,包含了接收端已经收到的seq区间信息,显然:

  1. 此协议需要接收端放置及发送端读取已收seq区间信息,因此需要双方设备均支持SACK。
  2. seq区间以外的seq就是接收端没收到、需要发送端重传的包,而Partial ACK对应的seq是待重传的第一个包。
    从而发送端收到Partial ACK后,可以推测出所有需重传的包,提高了效率。

Much More to Go

除了上述基础内容外,TCP等待我们探索的还有大量精彩的设计,可参考对应的RFC进行学习。